package de.hauschild.orbitvm.vm.binary;

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import de.hauschild.orbitvm.vm.instruction.Instruction;
import de.hauschild.orbitvm.vm.instruction.impl.dooble.Add;
import de.hauschild.orbitvm.vm.instruction.impl.dooble.Div;
import de.hauschild.orbitvm.vm.instruction.impl.dooble.Mult;
import de.hauschild.orbitvm.vm.instruction.impl.dooble.Output;
import de.hauschild.orbitvm.vm.instruction.impl.dooble.Phi;
import de.hauschild.orbitvm.vm.instruction.impl.dooble.Sub;
import de.hauschild.orbitvm.vm.instruction.impl.single.Cmpz;
import de.hauschild.orbitvm.vm.instruction.impl.single.Copy;
import de.hauschild.orbitvm.vm.instruction.impl.single.Input;
import de.hauschild.orbitvm.vm.instruction.impl.single.Noop;
import de.hauschild.orbitvm.vm.instruction.impl.single.Sqrt;
import de.hauschild.orbitvm.vm.instruction.impl.single.Cmpz.CompareMode;

/**
 * Loads the Orbit VM binaries.
 * 
 * @since 1.0.0
 * @author Klaus Hauschild
 */
public class BinaryLoader {

  private static final int MASK_TOP2 = 192;
  private static final int MASK_TOP4 = 240;
  private static final int MASK_BOTTOM4 = 15;
  private static final int MASK_BOTTOM6 = 63;

  private static final int OP_ADD = 1;
  private static final int OP_SUB = 2;
  private static final int OP_MULT = 3;
  private static final int OP_DIV = 4;
  private static final int OP_OUTPUT = 5;
  private static final int OP_PHI = 6;

  private static final int OP_NOOP = 0;
  private static final int OP_CMPZ = 1;
  private static final int OP_SQRT = 2;
  private static final int OP_COPY = 3;
  private static final int OP_INPUT = 4;

  private static final int IMM_LTZ = 0;
  private static final int IMM_LEZ = 1;
  private static final int IMM_EQZ = 2;
  private static final int IMM_GEZ = 3;
  private static final int IMM_GTZ = 4;

  private final List<Instruction> instructions = new ArrayList<Instruction>();
  private final List<Double> memory = new ArrayList<Double>();

  /**
   * Gets the instructions.
   * 
   * @return the instructions
   */
  public List<Instruction> getInstructions() {
    return new ArrayList<Instruction>(instructions);
  }

  /**
   * Gets the memory.
   * 
   * @return the memory
   */
  public Double[] getMemory() {
    final Double[] memoryArray = new Double[memory.size()];
    for (int i = 0; i < memory.size(); i++) {
      memoryArray[i] = memory.get(i);
    }
    return memoryArray;
  }

  /**
   * Load the binary.
   * 
   * @param dataInputStream
   *          the data input stream
   * @throws IOException
   *           Signals that an I/O exception has occurred.
   */
  public void load(final DataInputStream dataInputStream) throws IOException {
    try {
      while (true) {
        loadEven(dataInputStream);
        loadOdd(dataInputStream);
      }
    } catch (final EOFException eofe) {
      // reading complete
    }
  }

  /**
   * Load the data and instruction.
   * 
   * @param data
   *          the data
   * @param instruction
   *          the instruction
   */
  private void load(final double data, final int[] instruction) {
    memory.add(data);
    instructions.add(mapInstruction(instruction, instructions.size()));
  }

  /**
   * Load an even frame.
   * 
   * @param dataInputStream
   *          the data input stream
   * @throws IOException
   *           Signals that an I/O exception has occurred.
   * @throws EOFException
   *           the EOF exception
   */
  private void loadEven(final DataInputStream dataInputStream) throws IOException, EOFException {
    final double data = readData(dataInputStream);
    final int[] instruction = readInstruction(dataInputStream);
    load(data, instruction);
  }

  /**
   * Load an odd frame.
   * 
   * @param dataInputStream
   *          the data input stream
   * @throws IOException
   *           Signals that an I/O exception has occurred.
   * @throws EOFException
   *           the EOF exception
   */
  private void loadOdd(final DataInputStream dataInputStream) throws IOException, EOFException {
    final int[] instruction = readInstruction(dataInputStream);
    final double data = readData(dataInputStream);
    load(data, instruction);
  }

  /**
   * Map double instruction.
   * 
   * @param instruction
   *          the instruction
   * @param instructionCounter
   *          the instruction counter
   * @return the instruction
   */
  private Instruction mapDoubleInstruction(final int[] instruction, final int instructionCounter) {
    final int op = (instruction[0] & MASK_TOP4) >> 4;
    final int r1 = (instruction[0] & MASK_BOTTOM4) * (1 << 10) + instruction[1] * (1 << 2)
        + ((instruction[2] & MASK_TOP2) >> 6);
    final int r2 = (instruction[2] & MASK_BOTTOM6) * (1 << 8) + instruction[3];
    Instruction result = null;
    switch (op) {
    case OP_ADD:
      result = new Add(instructionCounter, r1, r2);
      break;
    case OP_SUB:
      result = new Sub(instructionCounter, r1, r2);
      break;
    case OP_MULT:
      result = new Mult(instructionCounter, r1, r2);
      break;
    case OP_DIV:
      result = new Div(instructionCounter, r1, r2);
      break;
    case OP_OUTPUT:
      result = new Output(instructionCounter, r1, r2);
      break;
    case OP_PHI:
      result = new Phi(instructionCounter, r1, r2);
      break;
    default:
      throw new IllegalArgumentException(String.format("Unknown opCode: %d.", op));
    }
    return result;
  }

  /**
   * Map immediate.
   * 
   * @param imm
   *          the immediate
   * @return the compare mode
   */
  private CompareMode mapImmediate(final int imm) {
    CompareMode compMode = null;
    switch (imm) {
    case IMM_LTZ:
      compMode = CompareMode.LTZ;
      break;
    case IMM_LEZ:
      compMode = CompareMode.LEZ;
      break;
    case IMM_EQZ:
      compMode = CompareMode.EQZ;
      break;
    case IMM_GEZ:
      compMode = CompareMode.GEZ;
      break;
    case IMM_GTZ:
      compMode = CompareMode.GTZ;
      break;
    default:
      throw new IllegalArgumentException(String.format("Unknown immidiate: %d.", imm));
    }
    return compMode;
  }

  /**
   * Map instruction.
   * 
   * @param instruction
   *          the instruction
   * @param instructionCounter
   *          the instruction counter
   * @return the instruction
   */
  private Instruction mapInstruction(final int[] instruction, final int instructionCounter) {
    final boolean isDoubleType = (instruction[0] & MASK_TOP4) != 0;
    if (isDoubleType) {
      return mapDoubleInstruction(instruction, instructionCounter);
    } else {
      return mapSingleInstruction(instruction, instructionCounter);
    }
  }

  /**
   * Map single instruction.
   * 
   * @param instruction
   *          the instruction
   * @param instructionCounter
   *          the instruction counter
   * @return the instruction
   */
  private Instruction mapSingleInstruction(final int[] instruction, final int instructionCounter) {
    final int op = (instruction[0] & MASK_BOTTOM4);
    final int imm = instruction[1] >> 5;
    final int r1 = (instruction[2] & MASK_BOTTOM6) * (1 << 8) + instruction[3];
    Instruction result = null;
    switch (op) {
    case OP_NOOP:
      result = new Noop(instructionCounter);
      break;
    case OP_CMPZ:
      result = new Cmpz(instructionCounter, r1, mapImmediate(imm));
      break;
    case OP_SQRT:
      result = new Sqrt(instructionCounter, r1);
      break;
    case OP_COPY:
      result = new Copy(instructionCounter, r1);
      break;
    case OP_INPUT:
      result = new Input(instructionCounter, r1);
      break;
    default:
      throw new IllegalArgumentException(String.format("Unknown opCode: %d.", op));
    }
    return result;
  }

  /**
   * Read data.
   * 
   * @param dataInputStream
   *          the data input stream
   * @return the double
   * @throws IOException
   *           Signals that an I/O exception has occurred.
   */
  private double readData(final DataInputStream dataInputStream) throws IOException {
    final byte[] w = new byte[8];
    dataInputStream.readFully(w, 0, 8);
    final long l = (long) (w[7]) << 56 | (long) (w[6] & 0xff) << 48 | (long) (w[5] & 0xff) << 40
        | (long) (w[4] & 0xff) << 32 | (long) (w[3] & 0xff) << 24 | (long) (w[2] & 0xff) << 16
        | (long) (w[1] & 0xff) << 8 | (w[0] & 0xff);
    return Double.longBitsToDouble(l);
  }

  /**
   * Read instruction.
   * 
   * @param dataInputStream
   *          the data input stream
   * @return the int[]
   * @throws IOException
   *           Signals that an I/O exception has occurred.
   */
  private int[] readInstruction(final DataInputStream dataInputStream) throws IOException {
    final int[] instruction = new int[4];
    for (int i = 3; i >= 0; i--) {
      instruction[i] = dataInputStream.read();
    }
    return instruction;
  }

}
